package server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;


public class MacheServer {

    private ServerSocket serverSocket;
    private Map<String, List<String>> convIDtoUsers;
    private Map<String, Connection> clients; 
    private Integer convCount;
    private BlockingQueue<Request> queue;

    /**
     * Constructor for the ChatServer
     * 
     * @param port
     * @throws IOException
     */
    public MacheServer(int port) throws IOException {
        serverSocket = new ServerSocket(port);
        clients = new HashMap<String, Connection>();
        convIDtoUsers = new HashMap<String, List<String>>();
        convCount = 0;
        queue = new ArrayBlockingQueue<Request>(10);
    }

    /**
     * Update conversation.
     * 
     * @param convID
     *            conversation id of the conversation to update
     * @param update
     *            the string representation of the update
     */
    private void updateConversation(String convID, final String update) {
        // get a list of clients to update
        List<String> usernames = convIDtoUsers.get(convID);
        for (String user : usernames) {
            final Connection conn = clients.get(user);
            try {
                updateClient(conn, update);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Helper method to update a client
     * 
     * @param conn
     *            the Connection instance that is associated with the client to
     *            update
     * @param update
     *            the string representation of update
     * @throws IOException
     *             if the client connection is broken
     */
    private void updateClient(Connection conn, String update)
            throws IOException {
        PrintWriter out = conn.getPrintWriter();
        out.print(update);
        out.flush();
    }

    /**
     * Helper methods to add users to a conversation
     * 
     * @param convId
     *            the conversation id of the conversation to update
     * @param usersToAdd
     *            a list of users to add to the conversation
     */
    private void addUserstoConv(String convId, ArrayList<String> usersToAdd) {

        List<String> existingMembers = convIDtoUsers.get(convId);
        List<String> newMembers = new ArrayList<String>();
        for (String m : usersToAdd) {
            if (!existingMembers.contains(m)) {
                newMembers.add(m);
            }
        }
        newMembers.addAll(existingMembers);
        convIDtoUsers.put(convId, newMembers);

    }

    /**
     * Serve method for the ChatServer. Start the consumer thread that takes a
     * request from the queue one at a time Start the producer which consists of
     * one thread handling one client connected. Put the client requests in to a
     * queue to be processed by the consumer. (IOExceptions from individual
     * clients do *not* terminate serve()).
     * 
     * @throws IOException
     *             if the main server socket is closed or broken
     */
    public void serve() throws IOException {

        // Start the consumer
        new Thread(new Runnable() {
            public void run() {
                try {
                    while (true) {
                        Request r = queue.take();
                        String response = handleRequest(r);
                        if (response != null) {
                            if (response.equals("quit")) {
                                // here the socket is not added to the hashmap
                                // or already removed
                                r.getConnection().close();
                            } else {
                                PrintWriter out = r.getConnection()
                                        .getPrintWriter();
                                out.print(response);
                                out.flush();
                            }
                        }
                    }
                } catch (InterruptedException e) {
                    System.out
                            .println("queue.take() is interrupted while waiting");
                } catch (IOException e) {
                    System.out
                            .println("an I/O error occurs when closing this socket");
                }
            }
        }).start();

        // Start the producer
        while (true) {

            final Socket client = serverSocket.accept();
            new Thread(new Runnable() {
                public void run() {
                    handleConnection(client);
                }
            }).start();
        }
    }

    /**
     * Remove the client from server. Invoked when the client exit the
     * application.
     * 
     * @param client
     */
    private void remove(String client) {
        clients.remove(client);
        // update all clients new active list
        String update = "activelist " + getActiveUsers() + "\n";
        Collection<Connection> clientsConnections = clients.values();
        for (Connection c : clientsConnections) {
            try {
                updateClient(c, update);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // remove user from convIDtoUsers
        // update each user in those conversations that user has logged off
        Set<String> keys = convIDtoUsers.keySet();
        for (String k : keys) {
            if (convIDtoUsers.get(k).contains(client)) {
                List<String> old = convIDtoUsers.get(k);
                List<String> newMembers = new ArrayList<String>();
                for (String m : old) {
                    if (!m.equals(client)) {
                        newMembers.add(m);
                    }
                }
                convIDtoUsers.put(k, newMembers);
                updateConversation(k, "logoff " + client + " " + k + "\n");
            }
        }
    }

    /**
     * Remove the user from the conversation
     * 
     * @param userid
     *            the userid of the user that is to be removed
     * @param convid
     *            the conversation id of the conversation in which the user is
     *            to be removed
     */
    private void removeFromConv(String userid, String convid) {
        // update each user in the conversation that user has exited the
        // conversation
        List<String> old = convIDtoUsers.get(convid);
        List<String> newMembers = new ArrayList<String>();
        for (String m : old) {
            if (!m.equals(userid)) {
                newMembers.add(m);
            }
        }
        convIDtoUsers.put(convid, newMembers);
        this.updateConversation(convid, "exit " + userid + " " + convid + "\n");
    }

    /**
     * Handle a single client connection. Do nothing but put client requests
     * into a thread-safe blocking queue.
     * 
     * @param socket
     *            socket where client is connected
     * @throws IOException
     *             if connection has an error or terminates unexpectedly
     */
    protected void handleConnection(Socket socket) {

        try {
            Connection client = new Connection(socket);
            BufferedReader in = client.getBufferedReader();

            for (String line = in.readLine(); line != null; line = in
                    .readLine()) {
                // put the request into a queue
                queue.put(new Request(client, line));
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (IOException e) {
            System.out.println("a user has logged off");
        }
    }

    /**
     * handler for client input
     * 
     * make requested mutations on UI if applicable, then return appropriate
     * message to the user
     * 
     * @param input
     * @socket socket where client is connected
     * @return message to the user
     */
    protected String handleRequest(Request r) {
        String input = r.getLine();
        Connection client = r.getConnection();
        String username = "[\\w]+";
        String regex = username + " "
                + "(start)|(end)|(invite)|(send)|(bye)|(get)|(active)";

        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(input);
        boolean found = m.find();
        if (!found) {
            String regex2 = "-new" + " " + username;
            if (input.matches(regex2)) {
                String[] tokens2 = input.split(" ");

                if (!clients.containsKey(tokens2[1])) {
                    try {
                        Collection<Connection> allConns = clients.values();
                        String activeUsers = getActiveUsers();
                        String broadcast = "activelist" + " "
                                + activeUsers.trim() + " " + tokens2[1] + "\n";
                        for (Object conn : allConns) {
                            this.updateClient((Connection) conn, broadcast);
                        }

                        clients.put(tokens2[1], client);
                        return "status okayID\n";
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                return "status errorID\n";
            }
        }

        String[] tokens = input.split(" ");
        String userid = tokens[0];

        if (tokens[1].equals("get")) {
            String convID = tokens[2];
            String currentUsers = "";
            List<String> users = convIDtoUsers.get(convID);

            for (String s : users) {
                currentUsers += s + " ";
            }

            return "usersinconv" + " " + tokens[2] + " " + currentUsers.trim()
                    + "\n";
        }

        else if (tokens[1].equals("start")) {
            ArrayList<String> users = new ArrayList<String>();
            String userlist = "";
            // new conversation must contain the initiator
            userlist += userid;
            users.add(userid);
            if (tokens.length >= 3) {
                for (int i = 2; i < tokens.length; i++) {
                    users.add(tokens[i]);
                    userlist += " " + tokens[i];
                }
            }

            String convid = "";
            convid = Integer.toString(convCount);
            convCount++;

            if (!convid.equals("")) {
                convIDtoUsers.put(convid, users);
                return "startconv " + convid + " " + userlist.trim() + "\n";
            }

        } else if (tokens[1].equals("end")) {
            String convID = tokens[2];
            this.removeFromConv(userid, convID);

        } else if (tokens[1].equals("invite")) {
            // must contain more than one user to add
            String convID = tokens[2];
            if (tokens.length >= 4) {
                ArrayList<String> newUsers = new ArrayList<String>();
                for (int i = 3; i < tokens.length; i++) {
                    newUsers.add(tokens[i]);
                }
                addUserstoConv(convID, newUsers);
                String newUsersString = "";
                for (String u : newUsers) {
                    newUsersString += u + " ";
                }
                this.updateConversation(convID, "newinvites" + " " + convID
                        + " " + newUsersString.trim() + "\n");
            }

            String currentUsers = "";
            List<String> users = convIDtoUsers.get(tokens[2]);

            for (String s : users) {
                currentUsers += s + " ";
            }
            this.updateConversation(convID, "usersinconv" + " " + convID + " "
                    + currentUsers.trim() + "\n");

        } else if (tokens[1].equals("send")) {
            String convID = tokens[2];
            String font = tokens[3];
            String message = "";
            for (int count = 4; count < tokens.length; count++) {
                message = message + tokens[count] + " ";
            }
            this.updateConversation(convID, "text " + convID + " " + userid
                    + " " + font + " " + message.trim() + "\n");
            return null;

        } else if (tokens[1].equals("bye")) {
            this.remove(userid);
            return "quit";

        } else if (tokens[1].equals("active")) {
            String activeUsers = this.getActiveUsers();
            return "activelist" + " " + activeUsers + "\n";

        } else if (tokens[1].equals("activewin")) {
            String convID = tokens[2];
            String activeUsers = "";
            Object[] userArr = clients.keySet().toArray();
            for (int i = 0; i < userArr.length; i++) {
                activeUsers += userArr[i] + " ";
            }
            return "activelistwin" + " " + convID + " " + activeUsers.trim()
                    + "\n";
        } else if (tokens[1].equals("typestatus")) {
            String convID = tokens[2];
            String status = tokens[3];
            this.updateConversation(convID, "typestatus " + convID + " "
                    + userid + " " + status + "\n");
        } else if (tokens[1].equals("nudge")) {
            String convID = tokens[2];
            String friend = tokens[3];
            this.updateConversation(convID, "nudged " + convID + " " + userid
                    + " " + friend + "\n");
        }
        // should never get here
        return "";
    }

    /**
     * Helper Method to get all the active users.
     * 
     * @return active user list separated by spaces
     */
    protected String getActiveUsers() {
        Set<String> keys = clients.keySet();
        String userlist = "";
        for (String k : keys) {
            userlist += k + " ";
        }
        return userlist.trim();
    }

    /**
     * Terminate the server
     * 
     * @throws IOException
     *             if an I/O error occurs when closing the socket.
     */
    public void die() {
        try {
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

